page.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. 'use client';
  2. import { useEffect, useState } from 'react';
  3. import Link from 'next/link';
  4. import { useParams } from 'next/navigation';
  5. import { CheckCircle2 } from 'lucide-react';
  6. import { fetchApi, getDateTime } from '@/lib/utils/client';
  7. import Loading from '@/app/component/Loading';
  8. import useCart from '@/hooks/useCart';
  9. import type { OrderDetail } from '@/types/store';
  10. function formatPrice(n: number): string
  11. {
  12. return n.toLocaleString('ko-KR');
  13. }
  14. export default function OrderCompletePage()
  15. {
  16. const params = useParams<{ orderID: string }>();
  17. const orderID = Number(params.orderID);
  18. const { clear } = useCart();
  19. const [order, setOrder] = useState<OrderDetail|null>(null);
  20. const [loading, setLoading] = useState(true);
  21. useEffect(() => {
  22. if (Number.isNaN(orderID) || orderID <= 0) {
  23. setLoading(false);
  24. return;
  25. }
  26. (async () => {
  27. const res = await fetchApi<OrderDetail>(`/api/store/orders/${orderID}`, { silent: true });
  28. if (res.success && res.data) {
  29. setOrder(res.data);
  30. // 결제 성공 페이지 진입 시점에 cart 비우기 (사용자가 결제 완료를 확인한 시점)
  31. clear();
  32. }
  33. setLoading(false);
  34. })();
  35. // eslint-disable-next-line react-hooks/exhaustive-deps
  36. }, [orderID]);
  37. if (loading) {
  38. return <Loading />;
  39. }
  40. if (!order) {
  41. return (
  42. <div className='container mx-auto max-w-md px-4 py-20 text-center'>
  43. <p className='text-neutral-500 mb-4'>주문 정보를 찾을 수 없습니다.</p>
  44. <Link href='/orders' className='text-blue-600 dark:text-blue-400 hover:underline'>
  45. 주문 목록으로
  46. </Link>
  47. </div>
  48. );
  49. }
  50. const hasDigital = order.items.some(i => i.type === 2);
  51. const hasPhysical = order.items.some(i => i.type === 1);
  52. const guideText = hasDigital && !hasPhysical
  53. ? '쿠폰은 보관함에서 확인하실 수 있어요.'
  54. : hasPhysical && !hasDigital
  55. ? '배송 상태는 주문 목록에서 확인하실 수 있어요.'
  56. : '쿠폰은 보관함, 배송 상태는 주문 목록에서 확인하실 수 있어요.';
  57. return (
  58. <div className='container mx-auto max-w-2xl px-4 py-10'>
  59. {/* 헤더 */}
  60. <div className='text-center mb-8'>
  61. <CheckCircle2 className='size-16 text-green-500 mx-auto mb-3' />
  62. <h1 className='text-2xl font-bold mb-1'>주문이 완료되었습니다</h1>
  63. <p className='text-neutral-500 text-sm'>{guideText}</p>
  64. </div>
  65. {/* 주문 요약 카드 */}
  66. <div className='border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 overflow-hidden mb-6'>
  67. <div className='px-5 py-3 border-b border-neutral-200 dark:border-neutral-800 flex justify-between items-center gap-2 flex-wrap'>
  68. <div>
  69. <div className='text-xs text-neutral-500'>주문번호</div>
  70. <div className='font-mono font-semibold text-sm'>{order.orderNumber}</div>
  71. </div>
  72. <div className='text-right'>
  73. <div className='text-xs text-neutral-500'>결제 일시</div>
  74. <div className='text-sm'>{getDateTime(order.paidAt ?? order.createdAt)}</div>
  75. </div>
  76. </div>
  77. <table className='w-full text-sm'>
  78. <tbody>
  79. {order.items.map((item) => (
  80. <tr key={item.id} className='border-b border-neutral-100 dark:border-neutral-800/60 last:border-0'>
  81. <td className='px-5 py-3'>
  82. <div className='font-medium'>{item.productName}</div>
  83. <div className='text-xs text-neutral-500 mt-0.5'>
  84. {item.type === 2 ? '쿠폰' : '실물'} · 수량 {item.quantity}
  85. </div>
  86. </td>
  87. <td className='text-right px-5 py-3 font-mono whitespace-nowrap'>
  88. {formatPrice(item.unitPrice * item.quantity)}P
  89. </td>
  90. </tr>
  91. ))}
  92. </tbody>
  93. </table>
  94. <div className='px-5 py-3 border-t border-neutral-200 dark:border-neutral-800 flex justify-between items-center bg-neutral-50 dark:bg-neutral-800/40'>
  95. <span className='text-sm font-semibold'>결제 금액</span>
  96. <span className='text-lg font-bold text-red-600 dark:text-red-400 font-mono'>
  97. {formatPrice(order.totalAmount)}P
  98. </span>
  99. </div>
  100. {order.channelName && (
  101. <div className='px-5 py-2 text-xs text-neutral-500 border-t border-neutral-100 dark:border-neutral-800/60'>
  102. 후원 채널: <span className='font-semibold'>{order.channelName}</span>
  103. </div>
  104. )}
  105. </div>
  106. {/* CTA 버튼 */}
  107. <div className='flex flex-col sm:flex-row gap-2'>
  108. {hasDigital && (
  109. <Link
  110. href='/inventory'
  111. className={`flex-1 px-4 py-3 rounded text-center text-sm font-semibold transition-colors ${
  112. !hasPhysical
  113. ? 'bg-blue-600 text-white hover:bg-blue-700'
  114. : 'border border-blue-600 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-950'
  115. }`}
  116. >
  117. 보관함으로
  118. </Link>
  119. )}
  120. <Link
  121. href={`/orders/${order.id}`}
  122. className={`flex-1 px-4 py-3 rounded text-center text-sm font-semibold transition-colors ${
  123. hasPhysical
  124. ? 'bg-blue-600 text-white hover:bg-blue-700'
  125. : 'border border-neutral-300 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-800'
  126. }`}
  127. >
  128. 주문 상세로
  129. </Link>
  130. </div>
  131. <Link
  132. href='/store'
  133. className='text-xs text-blue-600 dark:text-blue-400 hover:underline mt-4 block text-center'
  134. >
  135. 쇼핑 계속하기 〉
  136. </Link>
  137. </div>
  138. );
  139. }